summaryrefslogtreecommitdiff
path: root/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-23 06:06:27 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-23 06:06:27 +0000
commitf9bfc82880212e1a13f6bbb28ecfc87b89346f26 (patch)
treeee792f340ebfa7eaf30d2e79f99f41213e5c5cf3 /app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx
parentedc0eabc8f5fc44408c28023ca155bd73ddf8183 (diff)
(김준회) 메뉴접근제어(부서별) 메뉴 구현
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx')
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx297
1 files changed, 297 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx
new file mode 100644
index 00000000..bf43e7a9
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx
@@ -0,0 +1,297 @@
+"use client";
+
+import * as React from "react";
+import { useState, useTransition, useEffect } from "react";
+import { Settings, Plus, Users } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+import { DepartmentTreeView } from "./department-tree-view";
+import { DepartmentDomainAssignmentDialog } from "./department-domain-assignment-dialog";
+import {
+ type DepartmentNode
+} from "@/lib/users/knox-service";
+import {
+ assignDomainToDepartments,
+ getDepartmentDomainAssignments,
+ type UserDomain
+} from "@/lib/users/department-domain/service";
+import { DOMAIN_OPTIONS } from "./domain-constants";
+
+interface DepartmentMenuAccessManagerProps {
+ departmentsPromise: Promise<DepartmentNode[]>;
+ companyInfo: { code: string; name: string };
+}
+
+interface DepartmentAssignment {
+ id: number;
+ departmentCode: string;
+ departmentName: string;
+ assignedDomain: string;
+ description?: string | null;
+}
+
+export function DepartmentMenuAccessManager({
+ departmentsPromise,
+ companyInfo
+}: DepartmentMenuAccessManagerProps) {
+ const [departments, setDepartments] = useState<DepartmentNode[]>([]);
+ const [selectedDepartments, setSelectedDepartments] = useState<string[]>([]);
+ const [assignments, setAssignments] = useState<DepartmentAssignment[]>([]);
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [isPending, startTransition] = useTransition();
+ const [isDepartmentsLoading, setIsDepartmentsLoading] = useState(true);
+ const [isAssignmentsLoading, setIsAssignmentsLoading] = useState(true);
+
+ // Promise를 해결하여 부서 데이터 로드
+ useEffect(() => {
+ const loadDepartments = async () => {
+ setIsDepartmentsLoading(true);
+ try {
+ const departmentTree = await departmentsPromise;
+ setDepartments(departmentTree);
+ } catch (error) {
+ console.error("부서 트리 로드 실패:", error);
+ toast.error("부서 정보를 불러오는데 실패했습니다.");
+ setDepartments([]);
+ } finally {
+ setIsDepartmentsLoading(false);
+ }
+ };
+
+ loadDepartments();
+ }, [departmentsPromise]);
+
+ // 기존 할당 정보 로드
+ useEffect(() => {
+ const loadAssignments = async () => {
+ setIsAssignmentsLoading(true);
+ try {
+ const assignmentData = await getDepartmentDomainAssignments();
+ setAssignments(assignmentData as DepartmentAssignment[]);
+ } catch (error) {
+ console.error("할당 정보 로드 실패:", error);
+ toast.error("할당 정보를 불러오는데 실패했습니다.");
+ setAssignments([]);
+ } finally {
+ setIsAssignmentsLoading(false);
+ }
+ };
+
+ loadAssignments();
+ }, []);
+
+ // 선택된 부서들의 정보 가져오기
+ const getSelectedDepartmentInfo = React.useCallback(() => {
+ const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => {
+ for (const node of nodes) {
+ if (node.departmentCode === code) {
+ return node;
+ }
+ const found = findDepartment(node.children, code);
+ if (found) return found;
+ }
+ return null;
+ };
+
+ return selectedDepartments
+ .map(code => findDepartment(departments, code))
+ .filter(Boolean) as DepartmentNode[];
+ }, [departments, selectedDepartments]);
+
+ // 도메인 할당 처리
+ const handleDomainAssign = async (assignmentData: {
+ departmentCodes: string[];
+ domain: string;
+ description?: string;
+ }) => {
+ // 선택된 부서들의 이름 매핑 생성
+ const departmentNames: Record<string, string> = {};
+ const collectDepartmentNames = (nodes: DepartmentNode[]) => {
+ nodes.forEach(node => {
+ if (assignmentData.departmentCodes.includes(node.departmentCode)) {
+ departmentNames[node.departmentCode] = node.departmentName || node.departmentCode;
+ }
+ collectDepartmentNames(node.children);
+ });
+ };
+ collectDepartmentNames(departments);
+
+ startTransition(async () => {
+ try {
+ const result = await assignDomainToDepartments({
+ departmentCodes: assignmentData.departmentCodes,
+ domain: assignmentData.domain as UserDomain,
+ description: assignmentData.description,
+ departmentNames,
+ });
+
+ if (result.success) {
+ toast.success(result.message);
+ setSelectedDepartments([]);
+
+ // 할당 정보 새로고침
+ try {
+ const updatedAssignments = await getDepartmentDomainAssignments();
+ setAssignments(updatedAssignments as DepartmentAssignment[]);
+ } catch (error) {
+ console.error("할당 정보 새로고침 실패:", error);
+ }
+ } else {
+ toast.error(result.message);
+ }
+ } catch (error) {
+ console.error("도메인 할당 실패:", error);
+ toast.error("도메인 할당 중 오류가 발생했습니다.");
+ }
+ });
+ };
+
+ const canAssign = selectedDepartments.length > 0;
+ const selectedDepartmentInfo = getSelectedDepartmentInfo();
+
+ const isLoading = isDepartmentsLoading || isAssignmentsLoading;
+
+ return (
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
+ {/* 왼쪽: 조직도 트리 */}
+ <div className="lg:col-span-2">
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Settings className="h-5 w-5" />
+ 조직도 - {companyInfo.name}
+ </CardTitle>
+ <CardDescription>
+ 부서를 선택하여 도메인을 할당하세요. 상위 부서 선택 시 하위 부서들도 자동으로 포함됩니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="p-0">
+ {isLoading ? (
+ <div className="flex items-center justify-center h-[80vh]">
+ <div className="text-center">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
+ <p className="text-muted-foreground">조직도를 불러오는 중...</p>
+ </div>
+ </div>
+ ) : (
+ <DepartmentTreeView
+ departments={departments}
+ selectedDepartments={selectedDepartments}
+ onSelectionChange={setSelectedDepartments}
+ assignments={assignments}
+ />
+ )}
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 오른쪽: 선택된 부서 정보 및 할당 버튼 */}
+ <div className="space-y-6">
+ {/* 선택된 부서 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Users className="h-5 w-5" />
+ 선택된 부서
+ </CardTitle>
+ <CardDescription>
+ {selectedDepartments.length}개 부서가 선택되었습니다
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ {selectedDepartmentInfo.length === 0 ? (
+ <div className="text-center py-4 text-muted-foreground">
+ 부서를 선택해주세요
+ </div>
+ ) : (
+ <div className="space-y-2 max-h-60 overflow-y-auto">
+ {selectedDepartmentInfo.map((dept) => {
+ const assignment = assignments.find(a => a.departmentCode === dept.departmentCode);
+ return (
+ <div
+ key={dept.departmentCode}
+ className="flex items-center justify-between p-2 bg-accent/20 rounded-md"
+ >
+ <div className="min-w-0">
+ <div className="font-medium truncate">
+ {dept.departmentName || dept.departmentCode}
+ </div>
+ <div className="text-xs text-muted-foreground">
+ {dept.departmentCode}
+ </div>
+ </div>
+ {assignment && (
+ <Badge variant="outline" className="text-xs shrink-0">
+ {assignment.assignedDomain}
+ </Badge>
+ )}
+ </div>
+ );
+ })}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 도메인 할당 버튼 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">도메인 할당</CardTitle>
+ <CardDescription>
+ 선택된 부서들에 도메인을 할당합니다
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <Button
+ onClick={() => setIsDialogOpen(true)}
+ disabled={!canAssign || isPending}
+ size="lg"
+ className="w-full"
+ >
+ <Plus className="mr-2 h-4 w-4" />
+ 도메인 할당 ({selectedDepartments.length}개 부서)
+ </Button>
+
+ {canAssign && (
+ <div className="mt-3 text-sm text-muted-foreground">
+ 상위 부서를 선택한 경우 하위 부서들도 자동으로 동일한 도메인이 할당됩니다.
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 범례 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">도메인 범례</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 gap-2 text-sm">
+ {DOMAIN_OPTIONS.map((option) => (
+ <div key={option.value} className="flex items-center gap-2">
+ <Badge className={option.color}>
+ {option.value}
+ </Badge>
+ <span>{option.description}</span>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 도메인 할당 다이얼로그 */}
+ <DepartmentDomainAssignmentDialog
+ open={isDialogOpen}
+ onOpenChange={setIsDialogOpen}
+ selectedDepartments={selectedDepartments}
+ departments={departments}
+ companyInfo={companyInfo}
+ onAssign={handleDomainAssign}
+ isLoading={isPending}
+ />
+ </div>
+ );
+} \ No newline at end of file